//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 1995-2000, The Hypersonic SQL Group.
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Hypersonic SQL Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the Hypersonic SQL Group.
 *
 *
 * For work added by the HSQL Development Group:
 *
 * Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using EffiProzDB.Lib;



namespace EffiProzDB
{

    // fredt@users 20020225 - patch 1.7.0 by boucherb@users - named constraints
    // fredt@users 20020320 - doc 1.7.0 - update
    // tony_lai@users 20020820 - patch 595156 - violation of Integrity constraint name

    /**
     * Implementation of a table constraint with references to the indexes used
     * by the constraint.<p>
     *
     * @author Thomas Mueller (Hypersonic SQL Group)
     * @version 1.8.0
     * @since Hypersonic SQL
     */
    public class Constraint
    {

        /*
         SQL CLI codes

         Referential Constraint 0 CASCADE
         Referential Constraint 1 RESTRICT
         Referential Constraint 2 SET NULL
         Referential Constraint 3 NO ACTION
         Referential Constraint 4 SET DEFAULT
         */
        public const int CASCADE = 0,
                         SET_NULL = 2,
                         NO_ACTION = 3,
                         SET_DEFAULT = 4,
                         INIT_DEFERRED = 5,
                         INIT_IMMEDIATE = 6,
                         NOT_DEFERRABLE = 7;
        public const int FOREIGN_KEY = 0,
                         MAIN = 1,
                         UNIQUE = 2,
                         CHECK = 3,
                         PRIMARY_KEY = 4;
        public ConstraintCore core;
        public HsqlNameManager.HsqlName constName;
        public int constType;

        /**
         *  Constructor declaration for PK and UNIQUE
         */
        public Constraint(HsqlNameManager.HsqlName name, Table t, Index index, int type)
        {

            core = new ConstraintCore();
            constName = name;
            constType = type;
            core.mainTable = t;
            core.mainIndex = index;
            /* fredt - _in unique constraints column list for iColMain is the
               visible columns of iMain
             */
            core.mainColArray =  ArrayUtil.arraySlice(index.getColumns(), 0,
                    index.getVisibleColumns());
            core.colLen = core.mainColArray.Length;
        }

        /**
         *  Constructor for main constraints (foreign key references _in PK table)
         */
        public Constraint(HsqlNameManager.HsqlName name, Constraint fkconstraint)
        {

            constName = name;
            constType = MAIN;
            core = fkconstraint.core;
        }

        /**
         *  Constructor for foreign key constraints.
         *
         * @param  pkname name _in the main (referenced) table, used internally
         * @param  name name _in the referencing table, public name of the constraint
         * @param  mainTable referenced table
         * @param  refTable referencing talbe
         * @param  mainCols array of column indexes _in main table
         * @param  refCols array of column indexes _in referencing table
         * @param  mainIndex index on the main table
         * @param  refIndex index on the referencing table
         * @param  deleteAction triggered action on delete
         * @param  updateAction triggered action on update
         * @exception  HsqlException
         */
        public Constraint(HsqlNameManager.HsqlName pkname, HsqlNameManager.HsqlName name, Table mainTable,
                   Table refTable, int[] mainCols, int[] refCols,
                   Index mainIndex, Index refIndex, int deleteAction,
                   int updateAction)
        {

            core = new ConstraintCore();
            core.pkName = pkname;
            core.fkName = name;
            constName = name;
            constType = FOREIGN_KEY;
            core.mainTable = mainTable;
            core.refTable = refTable;
            /* fredt - _in FK constraints column lists for iColMain and iColRef have
               identical sets to visible columns of iMain and iRef respectively
               but the order of columns can be different and must be preserved
             */
            core.mainColArray = mainCols;
            core.colLen = core.mainColArray.Length;
            core.refColArray = refCols;
            core.mainIndex = mainIndex;
            core.refIndex = refIndex;
            core.deleteAction = deleteAction;
            core.updateAction = updateAction;
        }

        /**
         * temp constraint constructor
         */
        public Constraint(HsqlNameManager.HsqlName name, int[] mainCols, Table refTable, int[] refCols,
                   int type, int deleteAction, int updateAction)
        {

            core = new ConstraintCore();
            constName = name;
            constType = type;
            core.mainColArray = mainCols;
            core.refTable = refTable;
            core.refColArray = refCols;
            core.deleteAction = deleteAction;
            core.updateAction = updateAction;
        }

        private Constraint() { }

        /**
         * Returns the HsqlName.
         */
        public HsqlNameManager.HsqlName getName()
        {
            return constName;
        }

        /**
         * Changes constraint name.
         */
        private void setName(string name, bool isquoted)
        {
            constName.rename(name, isquoted);
        }

        /**
         *  probably a misnomer, but DatabaseMetaData.getCrossReference specifies
         *  it this way (I suppose because most FKs are declared against the PK of
         *  another table)
         *
         *  @return name of the index refereneced by a foreign key
         */
        public string getPkName()
        {
            return core.pkName == null ? null
                                       : core.pkName.name;
        }

        /**
         *  probably a misnomer, but DatabaseMetaData.getCrossReference specifies
         *  it this way (I suppose because most FKs are declared against the PK of
         *  another table)
         *
         *  @return name of the index for the referencing foreign key
         */
        public string getFkName()
        {
            return core.fkName == null ? null
                                       : core.fkName.name;

        }

        /**
         *  Returns the type of constraint
         */
        public int getType()
        {
            return constType;
        }

        /**
         *  Returns the main table
         */
        public Table getMain()
        {
            return core.mainTable;
        }

        /**
         *  Returns the main index
         */
        public Index getMainIndex()
        {
            return core.mainIndex;
        }

        /**
         *  Returns the reference table
         */
        public Table getRef()
        {
            return core.refTable; 

        }

        /**
         *  Returns the reference index
         */
        public Index getRefIndex()
        {
            return core.refIndex;
        }

        /**
         *  The ON DELETE triggered action of (foreign key) constraint
         */
        public int getDeleteAction()
        {
            return core.deleteAction;
        }

        /**
         *  The ON UPDATE triggered action of (foreign key) constraint
         */
        public int getUpdateAction()
        {
            return core.updateAction;
        }

        /**
         *  Returns the main table column index array
         */
        public int[] getMainColumns()
        {
            return core.mainColArray;
        }

        /**
         *  Returns the reference table column index array
         */
        public int[] getRefColumns()
        {
            return core.refColArray;
        }

        /**
         *  Returns true if an index is part this constraint and the constraint is set for
         *  a foreign key. Used for tests before dropping an index.
         */
        public bool isIndexFK(Index index)
        {

            if (constType == FOREIGN_KEY || constType == MAIN)
            {
                if (core.mainIndex == index || core.refIndex == index)
                {
                    return true;
                }
            }

            return false;
        }

        /**
         *  Returns true if an index is part this constraint and the constraint is set for
         *  a unique constraint. Used for tests before dropping an index.
         */
        public bool isIndexUnique(Index index)
        {
            return (constType == UNIQUE && core.mainIndex == index);
        }

        /**
         * Only for check constraints
         */
        public bool hasColumn(Table table, string colname)
        {

            if (constType != CHECK)
            {
                return false;
            }

            Expression.Collector coll = new Expression.Collector();

            coll.addAll(core.check, Expression.COLUMN);

          

            foreach (var e in coll )
            {     

                if (e.getColumnName().Equals(colname)
                        && table.tableName.name.Equals(e.getTableName()))
                {
                    return true;
                }
            }

            return false;
        }

        public bool hasColumn(int colIndex)
        {

            if (constType == MAIN)
            {
                return Array.IndexOf(core.mainColArray, colIndex) != -1;
            }
            else if (constType == FOREIGN_KEY)
            {
                return Array.IndexOf(core.refColArray, colIndex) != -1;
            }

            return false;
        }

        // fredt@users 20020225 - patch 1.7.0 by fredt - duplicate constraints

        /**
         * Compares this with another constraint column set. This implementation
         * only checks UNIQUE constraints.
         */
        public bool isEquivalent(int[] col, int type)
        {

            if (type != constType || constType != UNIQUE
                    || core.colLen != col.Length)
            {
                return false;
            }

            return ArrayUtil.haveEqualSets(core.mainColArray, col, core.colLen);
        }

        /**
         * Compares this with another constraint column set. This implementation
         * only checks FOREIGN KEY constraints.
         */
        public bool isEquivalent(Table tablemain, int[] colmain, Table tableref,
                             int[] colref)
        {

            if (constType != Constraint.MAIN
                    && constType != Constraint.FOREIGN_KEY)
            {
                return false;
            }

            if (tablemain != core.mainTable || tableref != core.refTable)
            {
                return false;
            }

            return ArrayUtil.areEqualSets(core.mainColArray, colmain)
                   && ArrayUtil.areEqualSets(core.refColArray, colref);
        }

        /**
         *  Used to update constrains to reflect structural changes _in a table.
         *  Prior checks must ensure that this method does not throw.
         *
         * @param  oldt reference to the old version of the table
         * @param  newt referenct to the new version of the table
         * @param  colindex index at which table column is added or removed
         * @param  adjust -1, 0, +1 to indicate if column is added or removed
         * @throws  HsqlException
         */
        public void replaceTable(Table oldt, Table newt, int colindex,
                          int adjust)
        {

            if (oldt == core.mainTable)
            {
                core.mainTable = newt;

                // exclude CHECK
                if (core.mainIndex != null)
                {
                    core.mainIndex =
                        core.mainTable.getIndex(core.mainIndex.getName().name);
                    core.mainColArray =
                        ArrayUtil.toAdjustedColumnArray(core.mainColArray,
                                                        colindex, adjust);
                }
            }

            if (oldt == core.refTable)
            {
                core.refTable = newt;

                if (core.refIndex != null)
                {
                    core.refIndex =
                        core.refTable.getIndex(core.refIndex.getName().name);

                    if (core.refIndex != core.mainIndex)
                    {
                        core.refColArray =
                            ArrayUtil.toAdjustedColumnArray(core.refColArray,
                                                            colindex, adjust);
                    }
                }
            }
        }

        /**
         * Checks for foreign key or check constraint violation when
         * inserting a row into the child table.
         */
        public void checkInsert(Session session, Object[] row)
        {

            if (constType == Constraint.MAIN || constType == Constraint.UNIQUE
                    || constType == Constraint.PRIMARY_KEY)
            {

                // inserts _in the main table are never a problem
                // unique constraints are checked by the unique index
                return;
            }

            if (constType == Constraint.CHECK)
            {
                checkCheckConstraint(session, row);

                return;
            }

            if (Index.isNull(row, core.refColArray))
            {
                return;
            }

            // a record must exist _in the main table
            bool exists = core.mainIndex.exists(session, row,
                                                   core.refColArray);

            if (!exists)
            {

                // special case: self referencing table and self referencing row
                if (core.mainTable == core.refTable)
                {
                    bool match = true;

                    for (int i = 0; i < core.colLen; i++)
                    {
                        if (!row[core.refColArray[i]].Equals(
                                row[core.mainColArray[i]]))
                        {
                            match = false;

                            break;
                        }
                    }

                    if (match)
                    {
                        return;
                    }
                }

                throw Trace.error(Trace.INTEGRITY_CONSTRAINT_VIOLATION_NOPARENT,
                                  Trace.Constraint_violation, new Object[] {
                core.fkName.name, core.mainTable.getName().name
            });
            }
        }

        /*
         * Tests a row against this CHECK constraint.
         */
        public void checkCheckConstraint(Session session,
                                  Object[] row)
        {

            core.checkFilter.currentData = row;

            bool match = true.Equals(core.check.test(session));

            core.checkFilter.currentData = null;

            if (!match)
            {
                throw Trace.error(Trace.CHECK_CONSTRAINT_VIOLATION,
                                  Trace.Constraint_violation, new Object[] {
                    constName.name, core.mainTable.tableName.name
                });
            }
        }

        // fredt@users 20020225 - patch 1.7.0 - cascading deletes

        /**
         * New method to find any referencing row for a
         * foreign key (finds row _in child table). If ON DELETE CASCADE is
         * supported by this constraint, then the method finds the first row
         * among the rows of the table ordered by the index and doesn't throw.
         * Without ON DELETE CASCADE, the method attempts to finds any row that
         * exists, _in which case it throws an exception. If no row is found,
         * null is returned.
         * (fredt@users)
         *
         * @param  row array of objects for a database row
         * @param  forDelete should we allow 'ON DELETE CASCADE' or 'ON UPDATE CASCADE'
         * @return Node object or null
         * @throws  HsqlException
         */
        public RowIterator findFkRef(Session session, Object[] row,
                              bool delete)
        {

            if (row == null || Index.isNull(row, core.mainColArray))
            {
                return core.refIndex.emptyIterator();
            }

            return delete
                   ? core.refIndex.findFirstRowForDelete(session, row,
                       core.mainColArray)
                   : core.refIndex.findFirstRow(session, row, core.mainColArray);
        }

        /**
         * For the candidate table row, finds any referring node _in the main table.
         * This is used to check referential integrity when updating a node. We
         * have to make sure that the main table still holds a valid main record.
         * If a valid row is found the corresponding <code>Node</code> is returned.
         * Otherwise a 'INTEGRITY VIOLATION' Exception gets thrown.
         */
        public bool hasMainRef(Session session, Object[] row)
        {

            if (Index.isNull(row, core.refColArray))
            {
                return false;
            }

            bool exists = core.mainIndex.exists(session, row,
                                                   core.refColArray);

            // -- there has to be a valid node _in the main table
            // --
            if (!exists)
            {
                throw Trace.error(Trace.INTEGRITY_CONSTRAINT_VIOLATION_NOPARENT,
                                  Trace.Constraint_violation, new Object[] {
                core.fkName.name, core.refTable.getName().name
            });
            }

            return exists;
        }

        /**
         * Test used before adding a new foreign key constraint. This method
         * returns true if the given row has a corresponding row _in the main
         * table. Also returns true if any column covered by the foreign key
         * constraint has a null value.
         */
        private static bool hasReferencedRow(Session session,
                Object[] rowdata, int[] rowColArray,
                Index mainIndex)
        {

            if (Index.isNull(rowdata, rowColArray))
            {
                return true;
            }

            // else a record must exist _in the main index
            return mainIndex.exists(session, rowdata, rowColArray);
        }

        /**
         * Check used before creating a new foreign key cosntraint, this method
         * checks all rows of a table to ensure they all have a corresponding
         * row _in the main table.
         */
        public static void checkReferencedRows(Session session, Table table,
                                        int[] rowColArray,
                                        Index mainIndex)
        {

            RowIterator it = table.getPrimaryIndex().firstRow(session);

            while (true)
            {
                Row row = it.next();

                if (row == null)
                {
                    break;
                }

                Object[] rowdata = row.getData();

                if (!Constraint.hasReferencedRow(session, rowdata, rowColArray,
                                                 mainIndex))
                {
                    string colvalues = "";

                    for (int i = 0; i < rowColArray.Length; i++)
                    {
                        Object o = rowdata[rowColArray[i]];

                        colvalues += o;
                        colvalues += ",";
                    }

                    throw Trace.error(
                        Trace.INTEGRITY_CONSTRAINT_VIOLATION_NOPARENT,
                        Trace.Constraint_violation, new Object[] {
                    colvalues, table.getName().name
                });
                }
            }
        }
    }
}
